1、为什么要有事件循环?
因为js是单线程的,事件循环是js的执行机制,也是js实现异步的一种方法。
既然js是单线程,那就像只有一个窗口的银行,客户需要排队一个一个办理业务,同理js任务也要一个一个顺序执行。如果一个任务耗时
过长,那么后一个任务也必须等着。那么问题来了,假如我们想浏览新闻,但是新闻包含的超清图片加载很慢,难道我们的网页要一直卡着
直到图片完全显示出来?因此聪明的程序员将任务分为两类:
- 同步任务
- 异步任务
当我们打开网站时,网页的渲染过程就是一大堆同步任务,比如页面骨架和页面元素的渲染。而像加载图片音乐之类占用资源大耗时久的任务,
就是异步任务。
2、宏任务与微任务
JavaScript中除了广泛的同步任务和异步任务,我们对任务有更精细的定义:
- macro-task(宏任务): 包括
整体代码script
,setTimeout
,setInterval
- micro-task(微任务):
Promise
,process.nextTick
不同的类型的任务会进入不同的Event Queue(事件队列),比如setTimeout、setInterval会进入一个事件队列,而Promise会进入
另一个事件队列。
一次事件循环中有宏任务队列和微任务队列。事件循环的顺序,决定js代码执行的顺序。进入整体代码(宏任务-<script>包裹的代码可以
理解为第一个宏任务),开始第一次循环,接着执行所有的微任务。然后再次从宏任务开始,找到其中一个任务队列的任务执行完毕,
再执行所有的微任务。如:
<script>
setTimeout(function() {
console.log('setTimeout');
})
new Promise(function(resolve) {
console.log('promise');
}).then(function() {
console.log('then');
})
console.log('console');
/* ----------------------------分析 start--------------------------------- */
1、`<script>`中的整段代码作为第一个宏任务,进入主线程。即开启第一次事件循环
2、遇到setTimeout,将其回调函数放入Event table中注册,然后分发到宏任务Event Queue中
3、接下来遇到new Promise、Promise,立即执行;将then函数分发到微任务Event Queue中。输出: promise
4、遇到console.log,立即执行。输出: console
5、整体代码作为第一个宏任务执行结束,此时去微任务队列中查看有哪些微任务,结果发现了then函数,然后将它推入主线程并执行。
输出: then
6、第一轮事件循环结束
开启第二轮事件循环。先从宏任务开始,去宏任务事件队列中查看有哪些宏任务,在宏任务事件队列中找到了setTimeout对应的回调函数,
立即执行之。此时宏任务事件队列中已经没有事件了,然后去微任务事件队列中查看是否有事件,结果没有。此时第二轮事件循环结束;
输出:setTimeout
/* ----------------------------分析 end--------------------------------- */
</script>
3、分析更复杂的代码
<script>
console.log('1');
setTimeout(function() {
console.log('2');
process.nextTick(function() {
console.log('3');
})
new Promise(function(resolve) {
console.log('4');
resolve();
}).then(function() {
console.log('5')
})
})
process.nextTick(function() {
console.log('6');
})
new Promise(function(resolve) {
console.log('7');
resolve();
}).then(function() {
console.log('8')
})
setTimeout(function() {
console.log('9');
process.nextTick(function() {
console.log('10');
})
new Promise(function(resolve) {
console.log('11');
resolve();
}).then(function() {
console.log('12')
})
})
</script>
一、第一轮事件循环
a)、整段<script>代码作为第一个宏任务进入主线程,即开启第一轮事件循环
b)、遇到console.log,立即执行。输出:1
c)、遇到setTimeout,将其回调函数放入Event table中注册,然后分发到宏任务事件队列中。我们将其标记为setTimeout1
d)、遇到process.nextTick,其回调函数放入Event table中注册,然后被分发到微任务事件队列中。记为process1
e)、遇到new Promise、Promise,立即执行;then回调函数放入Event table中注册,然后被分发到微任务事件队列中。记为then1。
输出: 7
f)、遇到setTimeout,将其回调函数放入Event table中注册,然后分发到宏任务事件队列中。我们将其标记为setTimeout2
此时第一轮事件循环宏任务结束,下表是第一轮事件循环宏任务结束时各Event Queue的情况
- | 宏任务事件队列 | 微任务事件队列 |
---|---|---|
第一轮事件循环 | (宏任务已结束) | process1、then1 |
第二轮事件循环(未开始) | setTimeout1 | |
第三轮事件循环(未开始) | setTimeout2 |
可以看到第一轮事件循环宏任务结束后微任务事件队列中还有两个事件待执行,因此这两个事件会被推入主线程,然后执行
g)、执行process1。输出:6
h)、执行then1。输出:8
第一轮事件循环正式结束!
二、第二轮事件循环
a)、第二轮事件循环从宏任务setTimeout1开始。遇到console.log,立即执行。输出: 2
b)、遇到process.nextTick,其回调函数放入Event table中注册,然后被分发到微任务事件队列中。记为process2
c)、遇到new Promise,立即执行;then回调函数放入Event table中注册,然后被分发到微任务事件队列中。记为then2。输出: 4
此时第二轮事件循环宏任务结束,下表是第二轮事件循环宏任务结束时各Event Queue的情况
- | 宏任务事件队列 | 微任务事件队列 |
---|---|---|
第一轮事件循环(已结束) | ||
第二轮事件循环 | (宏任务已结束) | process2、then2 |
第三轮事件循环(未开始) | setTimeout2 |
可以看到第二轮事件循环宏任务结束后微任务事件队列中还有两个事件待执行,因此这两个事件会被推入主线程,然后执行
d)、执行process2。输出:3
e)、执行then2。输出:5
第二轮事件循环正式结束!
三、第三轮事件循环
a)、第三轮事件循环从宏任务setTimeout2开始。遇到console.log,立即执行。输出: 9
d)、遇到process.nextTick,其回调函数放入Event table中注册,然后被分发到微任务事件队列中。记为process3
c)、遇到new Promise,立即执行;then回调函数放入Event table中注册,然后被分发到微任务事件队列中。记为then3。输出: 11
此时第三轮事件循环宏任务结束,下表是第三轮事件循环宏任务结束时各Event Queue的情况
- | 宏任务事件队列 | 微任务事件队列 |
---|---|---|
第一轮事件循环(已结束) | ||
第二轮事件循环(已结束) | ||
第三轮事件循环(未开始) | (宏任务已结束) | process3、then3 |
可以看到第二轮事件循环宏任务结束后微任务事件队列中还有两个事件待执行,因此这两个事件会被推入主线程,然后执行
d)、执行process3。输出:10
e)、执行then3。输出:12
4、js事件循环总结
1)、js执行从最先进入任务队列的宏任务开始,通常是整体<scirpt>代码
2)、宏任务队列事件全部执行完毕后,检查微任务队列是否有事件,有则执行,直到没有事件为止
3)、更新render(每一次事件循环,浏览器都可能会去更新渲染)
4)、重复以上步骤
5、参考文章
https://juejin.im/post/59e85e...
https://segmentfault.com/a/11...
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。